package com.niaobulashi.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.niaobulashi.common.utils.RedisUtils;
import com.niaobulashi.dao.SysCodeDao;
import com.niaobulashi.entity.SysCodeEntity;
import com.niaobulashi.service.SysCodeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @program: spring-boot-learning
 * @description: 数字字典实现类
 * @author: https://niaobulashi.com
 * @create: 2019-06-30 10:20
 */
@Service("sysCodeService")
public class SysCodeServiceImpl implements SysCodeService {

    private Logger logger = LoggerFactory.getLogger(SysCodeService.class);

    @Autowired
    private SysCodeDao sysCodeDao;

    @Autowired
    private RedisTemplate redisTemplate;

    public static final String codeHash = "codeHash";
    // 是否能连接到redis
    public static boolean can = true;
    public static int canRetry = 0;

    /**
     * 查询所有数字字典
     *
     * @return
     */
    @Override
    public List<SysCodeEntity> queryCodeAll() {
        logger.info("先从缓存中查找，如果没有则去数据进行查询");
        List<SysCodeEntity> codeList = new ArrayList<>();
        try {
            // 从redis 获取，获取不到就去数据库获取
            if (can) {
                getCodeAll(codeList);
            }
        } catch (QueryTimeoutException e) {
            logger.error("无法连接redis ", e);
            can = false;
        }
        if (!can) {
            /** redis down 无法连接，这个时候所有请求都访问数据库了，所谓的缓存雪崩
             * 解决方案
             * （1）redis高可用
             * 这个思想的含义是，既然redis有可能挂掉，那我多增设几台redis，这样一台挂掉之后其他的还可以继续工作，其实就是搭建的集群。
             */
            logger.error("redis 无法连接，去数据库查询了...");
            codeList = sysCodeDao.queryCodeAll();
        }
        return codeList;
    }

    private void getCodeAll(List<SysCodeEntity> codeList) {
        // List<SysCodeEntity> codeList = (List<SysCodeEntity>) redisTemplate.opsForList().leftPop("codeList");  // 只能查询一次

        // redisTemplate.opsForHash().keys(codeHash + "*") 错误写法

        Set<String> keys = redisTemplate.keys(codeHash + "*");
        for (String key : keys) {
            // 获取键值对
            Map<String, Object> o = redisTemplate.opsForHash().entries(key);
            codeList.add(JSONObject.parseObject(JSONObject.toJSONString(o), SysCodeEntity.class));
        }
        if (CollectionUtils.isEmpty(codeList)) {
            logger.info("说明缓存中没有数据，则到数据库中查询");
            codeList = sysCodeDao.queryCodeAll();

            logger.info("将数据库获取的数据存入缓存");
//            redisTemplate.opsForList().leftPush("codeList", codeList); // 只能查询一次

            for (SysCodeEntity sysCodeEntity : codeList) {
                JSONObject object = createJsonObject(sysCodeEntity);
                redisTemplate.opsForHash().putAll(codeHash + "_" + sysCodeEntity.getId(), object);
                // 这里设置相同的过期时间，如果在过期时间结束同时有请求，则造成缓存雪崩，解决就是加随机时间
                redisTemplate.expire(codeHash + "_" + sysCodeEntity.getId(), 10, TimeUnit.MINUTES);
            }
        } else {
            logger.info("redis 缓存中存在，直接从缓存中获取数据");
        }
    }

    private JSONObject createJsonObject(SysCodeEntity sysCodeEntity) {
        JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(sysCodeEntity));
        Field[] fields = SysCodeEntity.class.getDeclaredFields();
        for (Field field : fields) {
            if (object.containsKey(field.getName())) {
                object.put(field.getName(), RedisUtils.toJson(object.get(field.getName())));
            }
        }
        return object;
    }


    @Override
    public List<SysCodeEntity> queryCodeAllNoCache() {
//        try {
//            Thread.sleep(5000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        System.out.println("service当前线程" + Thread.currentThread().getName());
        return sysCodeDao.queryCodeAll();
    }

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Override
    public List<SysCodeEntity> getAllJdbc(int i) {
        int result = 0;
        try {
            jdbcTemplate.getDataSource().getConnection().setAutoCommit(false);
            int count = jdbcTemplate.update("update sys_code set update_time =now() where id=1");
            Thread.sleep(1000 * i);
            jdbcTemplate.getDataSource().getConnection().commit();
        } catch (SQLException | InterruptedException e) {
            e.printStackTrace();
            try {
                jdbcTemplate.getDataSource().getConnection().rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        } finally {
            try {
                jdbcTemplate.getDataSource().getConnection().setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        System.out.println(result + " update rows ");
        return null;
    }

    /**
     * 测试缓存击穿
     * //     描述：
     * //缓存穿透是指缓存和数据库中都没有的数据，而用户不断发起请求，
     * // 如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者，攻击会导致数据库压力过大。
     * //
     * // 解决方案：
     * //    接口层增加校验，如用户鉴权校验，id做基础校验，id<=0的直接拦截；
     * //    从缓存取不到的数据，在数据库中也没有取到，这时也可以将key-value对写为key-null，
     * //    缓存有效时间可以设置短点，如30秒（设置太长会导致正常情况也没法使用）。这样可以防止攻击用户反复用同一个id暴力攻击
     **/
    @Override
    public SysCodeEntity getById(Integer id) {
        Map o = redisTemplate.opsForHash().entries(codeHash + "_" + id);
        SysCodeEntity entity = null;
        if (o.keySet().size() < 1) {
            if ("fail".equals(notExistKeyStore(id))) {
                logger.info("正在访问不存在的key redis 过滤，防止暴力攻击数据库 ,id :" + id);
                return entity;
            }
            logger.info("-------------查询数据库 id" + id);
            try {
                // 模拟查询耗时
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            entity = sysCodeDao.getById(id);
            // 如果不存在，缓存key
            if (entity == null) {
                notExistKeyStore(id);
            }
            return entity;
        }
        logger.info("-------------查询缓存 id" + id);
        entity = JSONObject.parseObject(JSONObject.toJSONString(o), SysCodeEntity.class);
        return entity;
    }

    @Override
    public String testLong() {

        long ori = 202010210000L;
        String redisKey = "testLong";

        long count;
        if (redisTemplate.opsForValue().get(redisKey) == null) {
//            redisTemplate.opsForValue().set(redisKey, ori);
            // 取出来是string  java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long
//            count = (long) redisTemplate.opsForValue().get(redisKey);
            // RedisTemplate increment 错误:ERR value is not an integer or out of range解决
            // https://blog.csdn.net/weixin_42829048/article/details/83989784

            count = redisTemplate.opsForValue().increment(redisKey, ori);
        } else {
            count = redisTemplate.opsForValue().increment(redisKey, 1L);
        }
        return count + "";
    }

    @Override
    public String type() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("测试", "测试");
        redisTemplate.opsForValue().set("3", jsonObject);
        return null;
    }

    SoftReference<Map<String, String>> softReference = new SoftReference<>(new HashMap<>());
    WeakReference<Map<String, String>> weakReference = new WeakReference<>(new HashMap<>());


    @Override
    public Object soft() {
        Map<String, String> map = softReference.get();
        if (map == null) {
            softReference = new SoftReference<>(map = new HashMap<>());
        }
        if (map.size() == 0) {
            map.put("code1", "lr");
            map.put("code2", "lr2");
            map.put("code3", "lr3");
        }
        return map;
    }

    @Override
    public Object ref(int s) {
        Map<String, String> ref = new HashMap<>();
        for (int i = 0; i < s; i++) {
            ref.put(i + "", i + "");
        }
        return ref;
    }

    @Override
    public Object weak() {
        Map<String, String> map = weakReference.get();
        if (map == null) {
            weakReference = new WeakReference<>(map = new HashMap<>());
        }
        if (map.size() == 0) {
            map.put("code1", "lr");
            map.put("code2", "lr2");
            map.put("code3", "lr3");
        }
        return map;
    }


    private String notExistKeyStore(int id) {
        String redisKey = "not_exist_id_" + id;
        long count = redisTemplate.opsForValue().increment(redisKey, 1);
        if (count == 1) {
            //代表当前用户是第一次访问.
            //对当前的key设置一个五分钟的有效期,
            //我认为： 为什么设置有效期呢？不用清理
            redisTemplate.expire(redisKey, 1, TimeUnit.MINUTES);
            return "success";
        }
        if (count > 1) {
            return "fail";
        }
        return "fail";
    }
}
